# 第十八章 前端商品库管理

在本章节我们将实现商品库管理的前端相关页面模块,这个章节可以说是一次很好的检验前面内容掌握程度的机会。为什么这么说呢?对于本专栏的大纲内容而言,在前端部分的内容里,最复杂的就数这个商品库管理了。因为我们的每个商品涉及到很多属性(商品名称、价格、库存、规格、图片等),而这些属性都有存在改动的可能。这也就意味着我们的页面元素、交互会相对于之前我们之前已经实现了的页面模块会更复杂。但在上一章节最后的章节回顾中我也提到了,复杂不等于困难。我们本章节的实现思路、手段也都是在前面各个章节中已经接触过的了,我们更多的也还是在复用前面的知识,就看读者们在前面的章节内容掌握了多少了。

# 查询商品列表

这一小节我们先来实现一个商品列表页面,让我们的商品信息先能够展示出来。按照以往的风格,我们会在页面中放置一个新增商品的按钮,接着是放一个表格用于渲染商品数据。按照这个思路,我们创建一下相应的文件和配置。首先在前端工程目录下的src/views/product目录下我们再新增一个目录,也叫product,接着我们在这个目录下新增一个List.vue文件并加入如下基础代码:

<!-- src/views/product/product/List.vue -->
<template>
    <div class="lin-container">
        <div class="lin-title">商品列表</div>
        <div class="button-container">
            <el-button type="primary" v-auth="['新增商品']" @click="handleAdd">新增商品</el-button>
        </div>
        <div class="table-container">
            <el-table v-loading="loading" :data="productList">
                <el-table-column type="index" width="80"></el-table-column>
                <el-table-column label="商品图片" align="center" width="180">
                    <template slot-scope="props">
                        <img :src="props.row.main_img_url">
                    </template>
                </el-table-column>
                <el-table-column label="商品名称" prop="name" width="180"/>
                <el-table-column label="所属分类" prop="category.name" width="180"/>
                <el-table-column label="商品单价" prop="price" width="180"/>
                <el-table-column label="商品库存" prop="stock" width="180"/>
                <el-table-column label="商品概要" prop="summary" width="180"/>
                <el-table-column label="商品状态" prop="status" width="110">
                    <template slot-scope="props">
                        <el-tag :type=" props.row.status ? 'success' :'info'">{{ props.row.status ? '上架中' :'已下架' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="260">
                    <template slot-scope="props">
                        <el-button :type="props.row.status ? 'warning' :'success'"v-auth="['商品上架/下架']" plain size="mini"
                                   @click="handleChangeStatus(props.row.id)">{{ props.row.status ? '下架':'上架' }}
                        </el-button>
                        <el-button type="primary" v-auth="['编辑商品']" plain size="mini" @click="handleEdit(props.row)">编辑</el-button>
                        <el-button v-auth="['删除商品']" type="danger" plain size="mini" @click="handleDelete(props.row.id)">删除</el-button>
                    </template>

                </el-table-column>
            </el-table>
        </div>
    </div>
</template>

<script>
export default {
  name: 'List',
  data() {
    return {
      loading: false,
      productList: [],
    }
  },
  methods: {

    getProductList() {},

    handleAdd() {},

    handleEdit() {},

    handleDelete() {},

    handleChangeStatus() {},
    
  },
}
</script>

<style lang="scss" scoped>
    .button-container {
        margin-top: 30px;
        padding-left: 30px;
    }

    .table-container {
        margin-top: 30px;
        padding-left: 30px;
        padding-right: 30px;

        .theme-img {
            height: 120px;
            width: auto;
        }
    }
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

这里我们给商品列表页面放置了一些基础代码,为了能够让页面正常显示,我们还需要给这个页面配置一个路由地址,打开src/config/stage/product.js文件,我们添加一个路由配置:

const productRouter = {
  route: null,
  name: null,
  title: '商品管理',
  type: 'folder', // 类型: folder, tab, view
  icon: 'iconfont icon-tushuguanli', // 菜单图标
  filePath: 'views/product/', // 文件路径
  order: 3,
  inNav: true,
  children: [
    {
      title: '商品分类',
      ........................
      ........................
      ........................
    },
    {
      title: '商品库',
      type: 'view',
      route: '/product/product',
      filePath: 'views/product/product/List.vue',
      inNav: true,
      icon: 'iconfont icon-tushuguanli',
    },
  ],
}

export default productRouter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

因为我们的商品库页面也是属于商品管理这个一级菜单下面的,所以我们在上一章节中创建的路由配置文件中增加一个二级菜单的配置即可。配置完成之后,我们就可以到浏览器中来看看效果了:

可以看到这里我们初步就把列表页面展示出来了,接着我们要做的就是把数据填充到表格中,这里我们给List.vue定义一个组件方法getProductList():

<!-- src/views/product/product/List.vue -->

<template>
  <!-- 省略模板代码 -->
</template>

<script>
export default {
  name: 'List',
  data() {
    return {
      loading: false,
      productList: [],
    }
  },
  methods: {

    // 获取商品数据列表
    getProductList() {
    },

    handleAdd() {},

    handleEdit() {},

    handleDelete() {},

    handleChangeStatus() {},
    
  },
}
</script>

<style lang="scss" scoped>
  /* 省略样式代码 */
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

按照以往的套路,这里我们要做的就是在Product模型类里面创建一个模型方法,模型方法会去发起一个HTTP请求后端相应的接口。然后我们会在这个getProductList()里面去调用这个模型方法,最后把请求到的结果绑定到我们前面预先定义好的数据对象productList来实现表格内容的填充。套路依旧是这个套路,不过这里我们稍微会增加一些新的东西——分页查询

首先我们可以简单的回忆一下我们在前面后端部分开发章节里,在做商品库管理这块内容的时候,我们是做了两个关于查询所有商品的接口,一个是分页形式的,一个是不分页形式的。其中不分页形式的接口是作为某些页面模块里用来做选项数据列表的(比如前端精选主题管理章节内容),而分页形式的接口就是用在我们这个小节的内容的。

正常电商项目中,我们的商品肯定是一定数量规模的,很难在一个有限的屏幕尺寸中全部展示。虽然可以通过滑动滚动条的方式,但未免体验过于糟糕。所以便有了分页加载数据的这种模式,这也是在真实项目中很常用的数据展示方式。好了,在对这里的实现做了一个简单的介绍之后,我们就开始来动手实现一下了。首先我们先到Produtct模型类中来创建一下这个支持分页查询的模型方法:

import {
  get,
} from '@/lin/plugins/axios'

class Product {

  handleError = true

  /**
   * 获取所有商品,不分页
   * @returns {Promise<*>}
   */
  async getProducts() {...}

  /**
   * 获取所有商品,分页
   * @param product_key 商品名称关键字,用于搜索商品
   * @param count 查询数量
   * @param page  查询起始页数,默认成第0页开始
   * @returns {Promise<*>}
   */
  async getProductsPaginate({ product_name, count = 10, page = 0 }) {
    const url = 'v1/product/paginate'
    let res
    if (product_name) {
      res = await get(url, {
        count,
        page,
        product_name,
        handleError: this.handleError,
      })
    } else {
      res = await get(url, {
        count,
        page,
        handleError: this.handleError,
      })
    }
    return res
  }
}

export default new Product()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

这里我们定义了一个getProductsPaginate()模型方法,这个方法接收一个对象参数,对象里面包含了product_namecountpage三个属性。具体的含义我已经在方法的注释中说明了。

product_name 这个属性我们暂时还没用到,后面内容会涉及,先定义好即可。

这里需要注意的是我们给count和page都提供了默认值,如果外层在调用这个模型方法时没有传入相应属性,则会按默认值发起一个从后端数据库中第0页开始查询并返回10条记录数据的请求。模型方法定义好之后,让我们回到List.vue中来调用一下:

<!-- src/views/product/product/List.vue -->

<template>
  <!-- 省略模板代码 -->
</template>

<script>
import product from '@/models/product'

export default {
  name: 'List',
  data() {...},
  // 通过生命周期函数调用获取数据列表的方法
  created() {
    this.getProductList()
  },
  methods: {

    // 获取商品数据列表
   async getProductList() {
       this.loading = true
       const res = await product.getProductsPaginate({}) 
       this.productList = res.collection
       this.loading = false
   },

    // 省略一些代码
    
  },
}
</script>

<style lang="scss" scoped>
  /* 省略样式代码 */
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

这里我们通过在生命周期函数created()里调用了刚刚定义好的模型方法来实现页面组件被创建后就发起请求获取数据。注意分页接口返回的数据格式,我们要取结果中的collection属性的值并把它绑定到我们的数据对象productList。然后我们可以先到浏览器中看一下效果:

这里可以看到,我们的表格中已经填充了相应的商品数据了。由于屏幕大小关系,这里从图片中只看到了4条数据记录,其实刚好是渲染了10条数据记录的,这说明我们的模型方法是可以正常调用的,但是这里我们还没有完成所有工作。前面我们说了,我们这个页面是以分页查询的形式展示数据的。这里数据是有了,但是“分页”去哪了?这里我们只看到了10条记录,但是很明显我们数据库中实际的商品数量是多于这个数目的。这里默认情况下是从第0页开始展示数据,那我们想继续往下看第1页的数据怎么办呢?这里页面连个可以点击切换下一页的按钮都没!莫急,我们这就来添加一个用于分页交互操作的页面组件——el-pagination:

<!-- src/views/product/product/List.vue -->

<template>
  <div class="lin-container">
        <div class="lin-title">商品列表</div>
        <div class="button-container">...</div>
        <div class="table-container">...</div>
        <!-- 分页组件 -->
        <div class="pagination">
            <el-pagination
                v-if="refreshPagination"
                @current-change="handleCurrentChange"
                :background="true"
                :page-size="pageCount"
                :current-page="currentPage"
                layout="total, prev, pager, next, jumper"
                :total="total_nums">
            </el-pagination>
        </div>
    </div>
</template>

<script>
  // 省略一堆代码
</script>

<style lang="scss" scoped>
  /* 省略样式代码 */
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

这里我们引入了一个Element UI提供的Pagination(opens new window) 分页组件,这个组件可以用于渲染分页相关交互的页面元素,比如页码排列展示、点击事件和总记录数等等。从定义可以看出,我们需要给这个组件传递一些参数,所以这里我们需要定义几个数据对象,同时我还需要监听分页页码发生改变时的事件:

<!-- src/views/product/product/List.vue -->

<template>
  <div class="lin-container">
        <div class="lin-title">商品列表</div>
        <div class="button-container">...</div>
        <div class="table-container">...</div>
        <!-- 分页组件 -->
        <div class="pagination">
            <el-pagination
                v-if="refreshPagination"
                @current-change="handleCurrentChange"
                :background="true"
                :page-size="pageCount"
                :current-page="currentPage"
                layout="total, prev, pager, next, jumper"
                :total="total_nums">
            </el-pagination>
        </div>
    </div>
</template>

<script>
import product from '@/models/product'

export default {
  name: 'List',
  data() {
    return {
      loading: false,
      productList: [],
      refreshPagination: true, // 用于避免因为组件缓存导致分页组件不加载数据的问题
      total_nums: 0, // 商品总数
      currentPage: 1, // 默认获取第一页的数据
      pageCount: 10, // 每页10条数据
    }
  },
  created() {...},
  methods: {

    // 获取商品数据列表
    getProductList() {
       this.loading = true
       const currentPage = this.currentPage - 1

       const res = await product.getProductsPaginate({
            count: this.pageCount,
            page: currentPage
       })

       this.productList = res.collection
       this.total_nums = res.total_nums

       this.loading = false
    },

    // 切换分页
    async handleCurrentChange(val) {
      this.currentPage = val
      await this.getProductList()
    },

    // 省略一堆代码
    
  },
}
</script>

<style lang="scss" scoped>
  /* 省略样式代码 */
  .pagination {
    display: flex;
    justify-content: flex-end;
    margin: 20px;
  }
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

这里我们定义了total_numscurrentPagepageCount三个数据对象,同时定义了一个handleCurrentChange()函数用于在切换分页页码的时候获取切换后的页码并调用我们一开始定义好的getProductList()函数。

这里注意在引入了分页组件之后getProductList()函数内部的实现也做了一些调整,主要是和分页组件实现一个联动。

接着我们就可以到浏览器中刷新一下页面来看一下效果:

可以看到这里我们页面的右下角就出现了一个分页组件,组件会根据我传递的参数内容渲染页面组件并提供一些交互能力。当我们点击不同页码的时候,会动态的调整页码参数并向后端发起请求查询数据。

读者们可以通过键盘的F12打开浏览器的开发者工具来观察网络请求参数变化。

到这里,我们最基本的商品列表查询就实现完毕了,但细心的你肯定知道,我们还有个功能没实现,就是搜索商品的功能。虽然我们通过分页的方式来优化我们的表格内容展示,但是在面对数量众多的商品时,我们假如想找某个商品也一样是如同大海捞针,所以我们就需要为运营人员提供一个可以根据商品名称关键字搜索商品的功能,这里我们在页面上添加输入框来实现:

<!-- src/views/product/product/List.vue -->

<template>
  <div class="lin-container">
        <div class="lin-title">商品列表</div>
        <div class="button-container">
              <el-button type="primary" v-auth="['新增商品']" @click="handleAdd">新增商品</el-button>
              <!-- 搜索框 -->
              <lin-search @query="onQueryChange" placeholder="搜索商品"/>
        </div>
        <div class="table-container">...</div>
        <div class="pagination">...</div>
    </div>
</template>

<script>
import LinSearch from '@/components/base/search/lin-search'
import product from '@/models/product'

export default {
  name: 'List',
  components: {
    LinSearch,
  },
  data() {
    return {
      loading: false,
      productList: [],
      refreshPagination: true, // 用于避免因为组件缓存导致分页组件不加载数据的问题
      total_nums: 0, // 商品总数
      currentPage: 1, // 默认获取第一页的数据
      pageCount: 10, // 每页10条数据
      searchKeyword: '', // 搜索框内容
    }
  },
  created() {...},
  methods: {

    // 获取商品数据列表
    getProductList() {
       this.loading = true
       const currentPage = this.currentPage - 1

       const res = await product.getProductsPaginate({
            // 传入输入的关键字
            product_name: this.searchKeyword,
            count: this.pageCount,
            page: currentPage
       })

       this.productList = res.collection
       this.total_nums = res.total_nums

       this.loading = false
    },

   // 搜索框内容变化时回调方法
   onQueryChange(query) {
      // 去掉输入内容的前后空格  
      this.searchKeyword = query.trim()
      this.getProductList()
    },
    // 省略一堆代码
    
  },
}
</script>

<style lang="scss" scoped>
  /* 省略样式代码 */
  .button-container {
      display: flex;
      justify-content: space-between;
      margin-top: 30px;
      padding-left: 30px;
      padding-right: 30px;
  }
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

这里我们在页面上引入了LinCMS 封装好的LinSearch组件,同时监听了组件的query事件,就是当输入框内容发生改变时,就会触onQueryChange()方法,在这个方法里我们会把输入框的内容绑定到当前页面的数据对象searchKeyword上,然后调用getProductList()方法来重新加载表格数据。这里我们同样需要对getProductList()方法的内部实现做进一步完善,就是在调用模型方法时候把this.searchKeyword赋值给模型方法接收的参数对象中的product_name属性。这样我们就实现了通过关键字搜索商品的功能,注意这里上面示例代码中,我们修改了原来的.button-container这个样式类实现:

到这里,我们的商品列表页面的实现就基本完成了,最后为了逻辑的严谨性,我们还是给查询商品列表数据方法的内部实现增加一个异常捕获:


    // 获取商品数据列表
    getProductList() {
       this.loading = true
       const currentPage = this.currentPage - 1
       try {
          const res = await product.getProductsPaginate({
            // 传入输入的关键字
            product_name: this.searchKeyword,
            count: this.pageCount,
            page: currentPage,
          })

          this.productList = res.collection
          this.total_nums = res.total_nums

        this.loading = false
      } catch (e) {
          this.loading = false
          this.productList = []
          this.total_nums = 0
      }
  },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

到这里,我们的商品列表页面的数据展示就已经算是开发完成了,过程还是相对轻松的。从下一小节开始,我们就将来逐一实现新增、编辑、删除、商品上/架的页面交互逻辑,我们下一小节再见。

最后更新: 2021-08-12 13:31:59
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页